﻿using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Threading;
using Lidgren.Network;
using NetBrick.Client.Handler;
using NetBrick.Common;
using NetBrick.Common.Packets;

namespace NetBrick.Client
{
    public abstract class Client : IClient
    {
        private readonly NetClient _client;
        private readonly Dictionary<ushort, PacketHandler> _packetHandlers;
        private RSACryptoServiceProvider _cryptoServiceProvider;
        public bool EncryptionAvailable { get; set; }

        public bool ShuttingDown { get; private set; }

        protected Client(string appIdentifier, bool useListenThread = true)
        {
            var config = new NetPeerConfiguration(appIdentifier);
            config.EnableMessageType(NetIncomingMessageType.Data);
            config.EnableMessageType(NetIncomingMessageType.StatusChanged);
            _client = new NetClient(config);
            _packetHandlers = new Dictionary<ushort, PacketHandler>();
            _client.Start();
            if (useListenThread)
                StartListenThread();
            InitClient();
        }

        private void StartListenThread()
        {
            var listenThread = new Thread(() =>
            {
                while (!ShuttingDown)
                {
                    Update();
                }
                // ReSharper disable once FunctionNeverReturns
            });
            listenThread.Start();
        }

        public void Update()
        {
            NetIncomingMessage incomingMessage;
            if ((incomingMessage = _client.ReadMessage()) == null) return;
            switch (incomingMessage.MessageType)
            {
                case NetIncomingMessageType.DebugMessage:
                case NetIncomingMessageType.VerboseDebugMessage:
                    DebugInfo(incomingMessage.ReadString());
                    break;
                case NetIncomingMessageType.WarningMessage:
                    DebugWarn(incomingMessage.ReadString());
                    break;
                case NetIncomingMessageType.ErrorMessage:
                case NetIncomingMessageType.Error:
                    DebugError(incomingMessage.ReadString());
                    break;
                case NetIncomingMessageType.StatusChanged:
                    var status = (NetConnectionStatus)incomingMessage.ReadByte();
                    DebugInfo("Status changed. Code: {0}", status);
                    switch (status)
                    {
                        case NetConnectionStatus.Connected:
                            OnConnect();
                            break;
                        case NetConnectionStatus.Disconnected:
                            OnDisconnect(incomingMessage.ReadString());
                            break;
                    }
                    break;
                case NetIncomingMessageType.Data:
                    Object packetData;
                    var packetCode = Packet.Instance.Deserialize(incomingMessage, out packetData);
                    if (!_packetHandlers.ContainsKey(packetCode)) return;
                    _packetHandlers[packetCode].Process(packetData);
                    break;
                default:
                    DebugWarn("Unknown message type {0}", incomingMessage.MessageType);
                    break;
            }
        }

        public void Shutdown()
        {
            ShuttingDown = true;
        }

        private void InitClient()
        {
            Packet.Instance.Register(NetBrickPacketCode.EstablishEncryptionRequest,
                typeof(EstablishEncryptionPacket.Request));
            Packet.Instance.Register(NetBrickPacketCode.EstablishEncryptionResponse,
                typeof(EstablishEncryptionPacket.Response));
            AddFrameworkPacketHandler(NetBrickPacketCode.EstablishEncryptionResponse,
                new EstablishEncryptionPacketHandler(this));
            Init();
        }

        /// <summary>
        ///     Called when everything is done initializing. This is where you do things like add handlers.
        /// </summary>
        protected abstract void Init();

        /// <summary>
        ///     Log with level "info".
        /// </summary>
        /// <param name="message">The formatted text to log.</param>
        /// <param name="arg">The parameters to format with.</param>
        public abstract void DebugInfo(string message, params object[] arg);

        /// <summary>
        ///     Log with level "warning".
        /// </summary>
        /// <param name="message">The formatted text to log.</param>
        /// <param name="arg">The parameters to format with.</param>
        public abstract void DebugWarn(string message, params object[] arg);

        /// <summary>
        ///     Log with level "error".
        /// </summary>
        /// <param name="message">The formatted text to log.</param>
        /// <param name="arg">The parameters to format with.</param>
        public abstract void DebugError(string message, params object[] arg);

        /// <summary>
        ///     Log with level "severe".
        /// </summary>
        /// <param name="message">The formatted text to log.</param>
        /// <param name="arg">The parameters to format with.</param>
        public abstract void DebugSevere(string message, params object[] arg);

        /// <summary>
        ///     Called when disconnected from the server.
        /// </summary>
        /// <param name="reason">Reason for disconnecting.</param>
        protected abstract void OnDisconnect(string reason);

        /// <summary>
        ///     Called when successfully connected to the server.
        /// </summary>
        protected abstract void OnConnect();

        public void Connect(string address, int port)
        {
            _client.Connect(address, port);
        }

        public void Disconnect()
        {
            Disconnect("Unspecified");
        }

        public void Disconnect(string reason)
        {
            _client.Disconnect(reason);
        }

        private void AddFrameworkPacketHandler(ushort packetCode, PacketHandler packetHandler)
        {
            if (packetCode > 256)
            {
                DebugWarn("Could not add packet handler for code {0}: codes more than 256 reserved for application.",
                    packetCode);
                return;
            }
            _packetHandlers.Add(packetCode, packetHandler);
        }

        public void AddPacketHandler(ushort packetCode, PacketHandler packetHandler)
        {
            if (packetCode <= 256)
            {
                DebugWarn("Could not add packet handler for code {0}: codes 0-256 reserved for framework.", packetCode);
                return;
            }
            _packetHandlers.Add(packetCode, packetHandler);
        }

        public void SendPacket(ushort packetCode, Object packetData, NetDeliveryMethod deliveryMethod)
        {
            var outgoingMessage = _client.CreateMessage();
            outgoingMessage = Packet.Instance.Serialize(packetCode, packetData, outgoingMessage);
            _client.SendMessage(outgoingMessage, deliveryMethod);
        }

        public void EstablishEncryption()
        {
            if (EncryptionAvailable)
            {
                DebugError("Encryption already available!");
                return;
            }
            var packet = new EstablishEncryptionPacket.Request();
            SendPacket(NetBrickPacketCode.EstablishEncryptionRequest, packet, NetDeliveryMethod.ReliableOrdered);
            DebugInfo("Initializing encryption.");
        }

        internal void InitEncryption(string keyXml)
        {
            _cryptoServiceProvider = new RSACryptoServiceProvider();
            _cryptoServiceProvider.FromXmlString(keyXml);
            EncryptionAvailable = true;
        }

        public byte[] Encrypt(byte[] data)
        {
            return !EncryptionAvailable ? new byte[0] : _cryptoServiceProvider.Encrypt(data, true);
        }
    }
}